This code is written by Christian Mosbæk Johannessen. It operationalizes Theo van Leeuwen’s “distinctive feature”-approach to typography, as well as elements of Andreas Stötzner’s “Signography”.
Table of Contents
1. Setting up
2. Deriving distinctive features from the two datasets
3. Summary Statistics
4. Principal Component Analysis
1.1 Load packages
This chunk loads the packages required to execute the notebook.
1.2. Load stroke data
In order to run this notebook, two different data frames need to be loaded. The first contains the measures we made of the overall proportions of the fonts as well as individual strokes. The other contains counts of shape occurences.
The first data frame consists of 30 different measures made on each of 147 fonts. Figure 1 illustrates where in each sample the measures were made:
Fig. 1
Notice in the data frame that one observation (row) corresponds with one of a given letter’s strokes, 13 observations for each of the 147 fonts. The individual strokes are groped by Typeface (for example “Absolute Beauty”) and identified by Stroke (H1, H2, H3 etc.). This dataframe contains the values that the case asks you to attempt to automatically reproduce.
df <- read.csv("data/typography_data.csv", header = T, sep = ";")
df <- as.data.frame(df)
1.3. Load shape data
The second dataset to be included is not one I expect you to deal with as part of this case. It contains counts of bounded shapes (shape envelopes) as well as occurrences of three types of shape features, Straights, Angles and Curves, as well as the sum of these occurrences (Density). Figure 2 is an illustration of the principle behind the annotation of shape features.
shpdf <- read.csv(file = "data/typography_shape_data.csv", header = T, sep = ";")
shpdf <- as.data.frame(shpdf)
attach(shpdf)
shpdf <- shpdf[order(Typeface, na.last = F),]
The next section of the script expands the dataframe df by deriving the variables Weight, Tension, Expansion, and Orientation from Van Leeuwen and Stötzner.
2.1. Weight
Deriving the average weight of all the strokes in a given font is a bit roundabout. First, all direct measures of stroke widths (for example 70 px and 113 px for stroke o1 in the figure) are converted to WSR (Weight Scale Rating) by relating them to the font’s X-height (384 px). This is done to establish a comparable reference of scale (lest image size and resolution becomes a factor in calculations). WSR expresses stroke width as a float between 0 and 1 (with one being a stroke of equal width to X-Height). Because many strokes, for example o1, are of uneven width, we then average over the narrowest and widest part of the stroke to arrive at a mean WSR.
Fig. 3
df$WSRnarrow <- df$narrow / df$Xheight
This calculates the Weight Scale Rating (WSR) of the narrowest measure of the stroke.
df$WSRwide <- df$wide / df$Xheight
This calculates the Weight Scale Rating (WSR) of the widest measure of the stroke.
df$Weight <- (df$WSRnarrow + df$WSRwide) / 2
This calculates the average WSR for the stroke.
2.2 Tension
The tension ratio of a stroke expresses the so-called “entasis” or gestural dynamics (for example the result of a change in force during the production of the stroke in hand writing) of the individual stroke by relating the narrowest and widest measurement of the stroke. In the script this is based on the original pixel measurements.
Fig. 4
df$Tension <- df$wide / df$narrow
2.3. Expansion
The expansion of a font expresses how broadly the letters sit on the baseline. It is derived by relating the width of the letter “o” to the x-height (based on an assumption that the look and feel of a font tends to be fairly homogenous. If one letter is designed to be broad, all letters will be).
Fig. 5
df$Expansion <- (df$Owidth / df$Xheight)
2.4. Orientation
The orientation of a font expresses the tallness of letters, that is to say, how long are the ascenders and descenders in relation to the body of the letter (x-height). It is derived here by relating a measure of the Vertical Orientation (vertical distance between the top-most and bottom-most parts of the sample) to the x-height.
Fig. 6
df$VertOrientation <- (df$Vorientation / df$Xheight)
2.5. begin building summary data frame for PCA
Create new data frame called summary.typeface with grouped calculations of means of slope, weight, tension, expansion and orientation (from df) using stats::aggregate(). One of the variables, Slope, based on a simple measurement of the angle between the stem of “k” and the base line (see fig. 7), reguires no further processing and is included as is.
Fig. 7.
summary.typeface <- aggregate(df[ , c("Index", "Slope" , "Weight" , "Tension" , "Expansion" , "VertOrientation")], by=list(df$Type , df$Typeface), FUN=mean)
summary.typeface <- as.data.frame (summary.typeface)
2.6. Connectivity
The connectivity of a font expresses the extent to which letters are connected to one another. Script and Handwritten, which emulates italic writing, tend to have a high degree of connectedness. Sans Serif, which contains no information about hand writing gestures, tend not be connected at all.
Expand summary.typeface with variable Connectivity which we’re fetching from the shape_data.csv file (unelegant, but was an afterthought).
summary.typeface$Connectivity <- shpdf$Connectivity / 5
features <- c("Type", "Typeface", "Index", "Slope", "Weight", "Tension", "Expansion", "VertOrientation", "Connectivity")
names(summary.typeface) <- features
rm(features) # A bit of housekeeping
2.7. Contrast
Contrast expresses the difference in weight of the thinnest and thickest stroke in a letter.
Because letters have a different number of strokes (“H” has three, “o” has one), automatically calculating Contrast between strokes is a bit more tricky than the previous features (which either took the stroke or the entire letter as their unit of analysis) and needs some further lines of code:
The next chunk simply calculates how many fonts are currently in the dataset (under the assumption that each font has 13 rows, one for each stroke):
observations <- nrow(df)/13
Because the way we identified strokes in the dataset (conflating letters and strokes in a single identifier, e.g. H1, H2) makes subsetting difficult, the next chunk adds a variable, Letter, and repeats a factor to build a new identifier (Turns out I could have done this more easily in OpenRefine).
Letter <- c(rep("H", 3), rep("p", 2), rep("k", 3), rep("x", 2), rep("o", 1), rep("a", 2)) # A bit of data wrangling adding a variable, letter, because the way we formated 'stroke' in the dataset makes subsetting difficult
df$Letter <- rep(Letter, observations)
rm(Letter, observations) # Housekeeping
The next chunk adds a script to identify strokes with minimum and maximum weight for each letter and calculate a contrast ratio (min / max):
contrast.ratio <- function(L){
chosen <- subset(df, df$Letter==L)
a <- sapply(split(chosen$Weight, chosen$Typeface), min)
b <- sapply(split(chosen$Weight, chosen$Typeface), max)
c <- b / a
return(c)
}
The next chunk creates a new intermediate dataframe, df.contrast, with columns for calculated differences for each letter (and adding the letter you want in place of ‘L’).
df.contrast <- data.frame(contrast.ratio("H"), contrast.ratio("p"), contrast.ratio("k"), contrast.ratio("x"), contrast.ratio("o"), contrast.ratio("a"))
Column names in df.contrast are hobbile, so rename them with friendlier letters:
column.headings <- c("H", "p", "k", "x", "o", "a")
names(df.contrast) <- column.headings
rm(column.headings, contrast.ratio)
Add a column, mean to df.contrast and fill with calculated means for each typeface:
df.contrast$mean <- rowSums(df.contrast) / ncol(df.contrast)
Fetch mean for each typeface from df.contrast and add as column Contrast to summary.typeface:
summary.typeface$Contrast <- df.contrast$mean
rm(df.contrast) # housekeeping
2.8. Shape
The next lines derive shape descriptors; (1) Density per region (Dr), (2) Straight as proportion of Density (SD), (3) Angle as proportion of Density (AD) and (4) Curve as proportion of Density (CD), and adds them to summary.
summary.typeface$Dr <- (shpdf$Straight + shpdf$Angle + shpdf$Curve) / shpdf$Regions
This adds Dr to the summary.
summary.typeface$SD <- shpdf$Straight / (shpdf$Straight + shpdf$Angle + shpdf$Curve)
This adds SD to the summary.
summary.typeface$AD <- shpdf$Angle / (shpdf$Straight + shpdf$Angle + shpdf$Curve)
This adds AD to the summary.
summary.typeface$CD <- shpdf$Curve / (shpdf$Straight + shpdf$Angle + shpdf$Curve)
This adds CD to the summary.
Before moving on to Principal Compåonent Analysis, here are statistical summaries for each of the five typographical classes, Sans, Serif, Slab Serif, Handwritten and Script.
3.1. Sans Serif Typefaces
summary.sans <- subset(summary.typeface, summary.typeface$Type == "Sans")
summary(summary.sans)
Type Typeface Index Slope Weight Tension Expansion
Length:24 Length:24 Min. :1 Min. :90 Min. :0.1019 Min. :1.025 Min. :0.6113
Class :character Class :character 1st Qu.:1 1st Qu.:90 1st Qu.:0.1394 1st Qu.:1.060 1st Qu.:0.8569
Mode :character Mode :character Median :1 Median :90 Median :0.1575 Median :1.084 Median :1.0118
Mean :1 Mean :90 Mean :0.1570 Mean :1.092 Mean :0.9498
3rd Qu.:1 3rd Qu.:90 3rd Qu.:0.1665 3rd Qu.:1.114 3rd Qu.:1.0524
Max. :1 Max. :90 Max. :0.2262 Max. :1.173 Max. :1.0667
VertOrientation Connectivity Contrast Dr SD AD CD
Min. :1.002 Min. :0 Min. :1.030 Min. : 8.614 Min. :0.2215 Min. :0.1049 Min. :0.09091
1st Qu.:1.769 1st Qu.:0 1st Qu.:1.053 1st Qu.:11.750 1st Qu.:0.4082 1st Qu.:0.4228 1st Qu.:0.11268
Median :1.822 Median :0 Median :1.064 Median :12.222 Median :0.4286 Median :0.4455 Median :0.12406
Mean :1.826 Mean :0 Mean :1.071 Mean :12.607 Mean :0.4064 Mean :0.3968 Mean :0.19678
3rd Qu.:1.896 3rd Qu.:0 3rd Qu.:1.090 3rd Qu.:12.583 3rd Qu.:0.4343 3rd Qu.:0.4537 3rd Qu.:0.16552
Max. :2.269 Max. :0 Max. :1.144 Max. :18.000 Max. :0.4636 Max. :0.4595 Max. :0.62346
3.2. Serif (Humanist) Typefaces
summary.serif <- subset(summary.typeface, summary.typeface$Type == "Serif")
summary(summary.serif)
Type Typeface Index Slope Weight Tension Expansion
Length:33 Length:33 Min. :2 Min. :90 Min. :0.05591 Min. :1.379 Min. :0.7011
Class :character Class :character 1st Qu.:2 1st Qu.:90 1st Qu.:0.13295 1st Qu.:1.690 1st Qu.:0.9737
Mode :character Mode :character Median :2 Median :90 Median :0.14700 Median :1.919 Median :1.0329
Mean :2 Mean :90 Mean :0.14908 Mean :2.251 Mean :1.0056
3rd Qu.:2 3rd Qu.:90 3rd Qu.:0.17009 3rd Qu.:2.529 3rd Qu.:1.0682
Max. :2 Max. :90 Max. :0.23401 Max. :7.575 Max. :1.2185
VertOrientation Connectivity Contrast Dr SD AD
Min. :1.000 Min. :0.00000 Min. :1.312 Min. :17.00 Min. :0.1326 Min. :0.06542
1st Qu.:1.954 1st Qu.:0.00000 1st Qu.:1.681 1st Qu.:23.90 1st Qu.:0.2455 1st Qu.:0.25379
Median :2.030 Median :0.00000 Median :1.921 Median :25.56 Median :0.2806 Median :0.29644
Mean :2.041 Mean :0.04242 Mean :2.227 Mean :25.68 Mean :0.2973 Mean :0.29918
3rd Qu.:2.133 3rd Qu.:0.00000 3rd Qu.:2.625 3rd Qu.:28.11 3rd Qu.:0.3502 3rd Qu.:0.36187
Max. :2.603 Max. :0.20000 Max. :5.187 Max. :31.25 Max. :0.4522 Max. :0.46324
CD
Min. :0.08456
1st Qu.:0.27426
Median :0.42292
Mean :0.40348
3rd Qu.:0.52096
Max. :0.70870
3.3. Slab Serif Typefaces
summary.slab <- subset(summary.typeface, summary.typeface$Type == "Slab")
summary(summary.slab)
Type Typeface Index Slope Weight Tension Expansion
Length:33 Length:33 Min. :3 Min. :90 Min. :0.08438 Min. :1.008 Min. :0.5413
Class :character Class :character 1st Qu.:3 1st Qu.:90 1st Qu.:0.15080 1st Qu.:1.109 1st Qu.:0.9626
Mode :character Mode :character Median :3 Median :90 Median :0.17886 Median :1.173 Median :1.0343
Mean :3 Mean :90 Mean :0.19402 Mean :1.236 Mean :1.0222
3rd Qu.:3 3rd Qu.:90 3rd Qu.:0.22524 3rd Qu.:1.280 3rd Qu.:1.0816
Max. :3 Max. :90 Max. :0.35503 Max. :2.368 Max. :1.6024
VertOrientation Connectivity Contrast Dr SD AD CD
Min. :1.000 Min. :0 Min. :1.011 Min. : 7.125 Min. :0.1239 Min. :0.04317 Min. :0.00000
1st Qu.:1.769 1st Qu.:0 1st Qu.:1.102 1st Qu.:23.111 1st Qu.:0.3626 1st Qu.:0.29388 1st Qu.:0.08108
Median :1.870 Median :0 Median :1.149 Median :24.333 Median :0.4384 Median :0.45455 Median :0.11330
Mean :1.834 Mean :0 Mean :1.189 Mean :23.985 Mean :0.3982 Mean :0.37655 Mean :0.22526
3rd Qu.:1.914 3rd Qu.:0 3rd Qu.:1.223 3rd Qu.:25.111 3rd Qu.:0.4554 3rd Qu.:0.47032 3rd Qu.:0.33333
Max. :2.188 Max. :0 Max. :1.703 Max. :34.800 Max. :0.5000 Max. :0.50000 Max. :0.80769
3.4. Handwritten Typefaces
summary.hand <- subset(summary.typeface, summary.typeface$Type == "Hand")
summary(summary.hand)
Type Typeface Index Slope Weight Tension Expansion
Length:22 Length:22 Min. :4 Min. : 95.0 Min. :0.05377 Min. :1.067 Min. :0.1957
Class :character Class :character 1st Qu.:4 1st Qu.:102.2 1st Qu.:0.10357 1st Qu.:1.424 1st Qu.:0.4476
Mode :character Mode :character Median :4 Median :108.0 Median :0.12756 Median :1.944 Median :0.6044
Mean :4 Mean :109.0 Mean :0.13221 Mean :2.149 Mean :0.5697
3rd Qu.:4 3rd Qu.:114.0 3rd Qu.:0.15940 3rd Qu.:2.425 3rd Qu.:0.6492
Max. :4 Max. :126.0 Max. :0.28110 Max. :4.763 Max. :1.0354
VertOrientation Connectivity Contrast Dr SD AD
Min. :1.517 Min. :0.0000 Min. :1.039 Min. : 6.939 Min. :0.04482 Min. :0.03306
1st Qu.:1.860 1st Qu.:0.2500 1st Qu.:1.133 1st Qu.:12.583 1st Qu.:0.09568 1st Qu.:0.12206
Median :2.249 Median :0.6000 Median :1.232 Median :15.972 Median :0.14535 Median :0.15774
Mean :2.317 Mean :0.5455 Mean :1.250 Mean :19.249 Mean :0.14993 Mean :0.17000
3rd Qu.:2.562 3rd Qu.:0.8000 3rd Qu.:1.351 3rd Qu.:24.542 3rd Qu.:0.18738 3rd Qu.:0.22052
Max. :4.198 Max. :1.0000 Max. :1.635 Max. :42.667 Max. :0.29630 Max. :0.43243
CD
Min. :0.2838
1st Qu.:0.5684
Median :0.7014
Mean :0.6801
3rd Qu.:0.7559
Max. :0.9186
3.5. Script Typefaces
summary.script <- subset(summary.typeface, summary.typeface$Type == "Script")
summary(summary.script)
Type Typeface Index Slope Weight Tension
Length:35 Length:35 Min. :5 Min. : 86.0 Min. :0.07272 Min. : 1.107
Class :character Class :character 1st Qu.:5 1st Qu.: 99.5 1st Qu.:0.09179 1st Qu.: 1.749
Mode :character Mode :character Median :5 Median :108.0 Median :0.13372 Median : 2.148
Mean :5 Mean :106.4 Mean :0.13423 Mean : 2.587
3rd Qu.:5 3rd Qu.:113.5 3rd Qu.:0.17170 3rd Qu.: 2.689
Max. :5 Max. :121.0 Max. :0.24585 Max. :14.408
Expansion VertOrientation Connectivity Contrast Dr SD
Min. :0.2411 Min. :0.9578 Min. :0.0000 Min. :1.056 Min. : 4.702 Min. :0.004246
1st Qu.:0.4367 1st Qu.:1.8998 1st Qu.:0.4000 1st Qu.:1.230 1st Qu.:10.452 1st Qu.:0.050377
Median :0.5571 Median :2.2313 Median :0.6000 Median :1.312 Median :18.286 Median :0.081081
Mean :0.5740 Mean :2.4403 Mean :0.6057 Mean :1.345 Mean :19.347 Mean :0.085487
3rd Qu.:0.6995 3rd Qu.:2.3885 3rd Qu.:0.8000 3rd Qu.:1.464 3rd Qu.:24.643 3rd Qu.:0.108854
Max. :0.9552 Max. :6.1803 Max. :1.0000 Max. :1.676 Max. :67.286 Max. :0.231884
AD CD
Min. :0.007363 Min. :0.5547
1st Qu.:0.042781 1st Qu.:0.6912
Median :0.122699 Median :0.7625
Mean :0.128530 Mean :0.7860
3rd Qu.:0.192302 3rd Qu.:0.9007
Max. :0.335938 Max. :0.9737
Using prcomp() with normalization (correlation matrix) by setting scale = TRUE (FALSE gives us a covariance matrix in stead):
res.pca <- prcomp(summary.typeface[ , c(4:14)], scale = T)
The next chunk outputs a scree plot to examine how many PCs we need to consider and what is noise:
get_eigenvalue(res.pca)
fviz_eig(res.pca) # Seems the first 3 are pretty relevant (74.7 % variance explained). Adding the 4th brings us to 88% variance explained.
Examine the results for Variables of the PCA
res.var <- get_pca_var(res.pca)
res.var$coord # Coordinates
Dim.1 Dim.2 Dim.3 Dim.4 Dim.5 Dim.6 Dim.7 Dim.8
Slope -0.77615564 -0.097714126 0.2462106 0.21386565 -0.01366866 -0.30053487 0.146898971 0.388050561
Weight 0.43093206 0.051914556 0.1765697 0.82800935 0.17181240 0.17318869 -0.134649524 -0.076759572
Tension -0.37714642 0.651001270 -0.2688038 0.22666748 0.16911367 -0.44471350 0.217409570 -0.166855755
Expansion 0.81162249 0.240471242 0.1240455 0.11395402 0.07780639 0.25017459 0.189057604 0.238184867
VertOrientation -0.42440175 0.008505285 0.5214649 -0.26168552 0.68241263 0.07964058 0.034618125 -0.077604057
Connectivity -0.81668178 -0.054272729 0.2592759 0.10387191 -0.10118690 -0.18514481 -0.370878656 0.013184892
Contrast 0.01657250 0.876624489 -0.2002308 -0.18316018 0.10914291 0.13335294 -0.282694783 0.187672307
Dr 0.04050132 0.522040022 0.6912037 -0.07593518 -0.45804150 0.04886883 0.101919593 -0.128116694
SD 0.94654120 -0.051791245 0.1013813 -0.10744219 0.05555730 -0.15698183 -0.008095973 0.036138915
AD 0.86594030 -0.037206074 0.1305609 -0.04776382 0.04465815 -0.42586082 -0.129692673 0.004703778
CD -0.93626467 0.045933977 -0.1199579 0.07998068 -0.05174420 0.30215606 0.071644680 -0.020989410
Dim.9 Dim.10 Dim.11
Slope -0.137770434 0.004991307 -1.220273e-31
Weight -0.106154487 0.004581275 3.816395e-32
Tension 0.086669692 -0.036543027 -9.466018e-33
Expansion 0.306514672 0.001578536 8.644617e-32
VertOrientation -0.008751778 0.008880375 2.279021e-32
Connectivity 0.253458793 -0.074755645 -3.744679e-32
Contrast -0.112575839 0.010445805 -1.068454e-32
Dr -0.056794372 0.009376637 -2.962442e-33
SD -0.085307164 -0.207789908 1.422958e-16
AD 0.038353904 0.166644640 1.443278e-16
CD 0.023809071 0.019889292 2.773448e-16
res.var$contrib # Contributions to the PCs
Dim.1 Dim.2 Dim.3 Dim.4 Dim.5 Dim.6 Dim.7 Dim.8
Slope 12.144350458 0.618371833 5.6225216 4.8944045 0.02427157 12.2259837 5.77308627 50.086250006
Weight 3.743641691 0.174547362 2.8916766 73.3648880 3.83490652 4.0600665 4.85042942 1.959778429
Tension 2.867455129 27.447239431 6.7017508 5.4978922 3.71537960 26.7703890 12.64526275 9.260283089
Expansion 13.279594169 3.745081663 1.4271835 1.3895594 0.78645951 8.4718931 9.56222242 18.869926755
VertOrientation 3.631039584 0.004685034 25.2213156 7.3278603 60.49784375 0.8585446 0.32061041 2.003137403
Connectivity 13.445668135 0.190764837 6.2350788 1.1545534 1.33013096 4.6399911 36.79884802 0.057822256
Contrast 0.005536721 49.769376060 3.7185974 3.5898799 1.54752208 2.4071283 21.37992272 11.714999347
Dr 0.033068538 17.649914130 44.3128484 0.6170265 27.25558135 0.3232645 2.77898310 5.459505805
SD 18.061578655 0.173719149 0.9533076 1.2352860 0.40098490 3.3357449 0.01753511 0.434402289
AD 15.116545826 0.089652557 1.5810448 0.2441270 0.25908801 24.5487526 4.49988509 0.007359284
CD 17.671521093 0.136647945 1.3346749 0.6845229 0.34783174 12.3582417 1.37321469 0.146535337
Dim.9 Dim.10 Dim.11
Slope 8.57906015 0.031699838 1.261933e-29
Weight 5.09335997 0.026705532 1.234324e-30
Tension 3.39517500 1.699172951 7.593766e-32
Expansion 42.46490889 0.003170573 6.333068e-30
VertOrientation 0.03461945 0.100343948 4.401688e-31
Connectivity 29.03637420 7.110768282 1.188370e-30
Contrast 5.72819827 0.138839286 9.674637e-32
Dr 1.45793536 0.111872352 7.437421e-33
SD 3.28926354 54.938581032 1.715960e+01
AD 0.66488543 35.335499575 1.765316e+01
CD 0.25621973 0.503346630 6.518724e+01
correlation_matrix <- as.data.frame(res.var$coord) # Store the correlations as a matrix we can examine and use in the article
PCs <- c("PC1", "PC2", "PC3", "PC4", "PC5", "PC6", "PC7", "PC8", "PC9", "PC10", "PC11") # Rename the variables from Dim.1 to PC1, which is more transparent
colnames(correlation_matrix) <- PCs
rm(PCs) # Housekeeping
Perform Kaiser-Meyer-Olkin test for measuring sampling adequacy. Result is miserable we’re only getting a MSA = 0.5
KMO(correlation_matrix)
Error in solve.default(r) :
system is computationally singular: reciprocal condition number = 5.52332e-18
matrix is not invertible, image not found
Kaiser-Meyer-Olkin factor adequacy
Call: KMO(r = correlation_matrix)
Overall MSA = 0.5
MSA for each item =
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10 PC11
0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5
Make fancy biplot of PC1 vs. PC2
type <- as.factor(summary.typeface$Type) # Subsets types for coloring the plot
fviz_pca_biplot(res.pca,
axes = c(1, 2),
col.ind = type,
geom = c("point", "text"),
label = "all", labelsize = 3, invisible = "none",
repel = FALSE,
habillage = "none",
palette = NULL,
addEllipses = TRUE, title = "PCA - Biplot"
)
rm(type) # Housekeeping
type <- as.factor(summary.typeface$Type) # Subsets types for coloring the plot
fviz_pca_biplot(res.pca,
axes = c(1, 3),
col.ind = type,
geom = c("point", "text"),
label = "all", labelsize = 3, invisible = "none",
repel = FALSE,
habillage = "none",
palette = NULL,
addEllipses = TRUE, title = "PCA - Biplot"
)
rm(type) # Housekeeping
Make less fancy biplots, trying to rotate the data to reveal other structures (these are the two most interesting ones)
ggbiplot(res.pca, choices = c(1,2), groups = summary.typeface$Type, ellipse = T, ellipse.prob = .95)
ggbiplot(res.pca, choices = c(1,3), groups = summary.typeface$Type, ellipse = T, ellipse.prob = .95)